iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Software Development

Vibe Unity - AI時代的遊戲開發工作流系列 第 15

Day 15 - Fal AI API網站功能介紹

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250928/20119470WhrnVBqG8T.png

Fal AI 是一个集結了各種 AI API 工具的整合平台
是目前最快最全面的圖片, 影片生成技術平台

你可以選擇一個模型, 直接在網站上試玩看看:
https://ithelp.ithome.com.tw/upload/images/20250928/20119470u2UILkv9pa.png


在 Unity 中使用的代碼:

https://ithelp.ithome.com.tw/upload/images/20250928/20119470IJxjmgX6gO.png

把代碼插入 Inspector 之中

// FalAIDemo_Newtonsoft.cs
// 單一腳本:Inspector 拖入 Buttons / InputFields / RawImage,即可測各模型
// 需安裝 com.unity.nuget.newtonsoft-json
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class FalAIDemo_Newtonsoft : MonoBehaviour
{
    [Header("Fal API")]
    [Tooltip("在伺服器/Editor 設定,勿打包到行動端/前端")]
    public string falApiKey = "PASTE_YOUR_FAL_KEY_HERE";
    [Tooltip("Fal 同步端點前綴,不用改")]
    public string falBaseUrl = "https://fal.run/";

    [Header("UI - Common Inputs")]
    public InputField promptInput;       // 文字提示
    public InputField imageUrlInput;     // 影像/貼圖 URL
    public RawImage previewImage;        // 顯示回傳影像(若有)
    public Text statusText;              // 顯示狀態/錯誤訊息

    [Header("UI - Buttons (拖進來)")]
    public Button btnFastLightningSDXL;
    public Button btnTripo3D;
    public Button btnFlux;
    public Button btnGeminiEdit;
    public Button btnAuraSR;
    public Button btnKlingImageToVideo;
    public Button btnWanEffects;
    public Button btnAnyLLM;
    public Button btnElevenlabsSFX;
    public Button btnCassetteMusic;
    public Button btnSonautoMusic;
    public Button btnImagen4Fast;
    public Button btnGeminiGenerate;
    public Button btnAnyLLMVision;

    // === 推薦端點常數(可依需要調整) ===
    const string END_FAST_LIGHTNING_SDXL = "fal-ai/fast-lightning-sdxl";
    const string END_TRIPO_3D            = "tripo3d/tripo/v2.5/image-to-3d";
    const string END_FLUX_DEV            = "fal-ai/flux/dev";
    const string END_GEMINI_EDIT         = "fal-ai/gemini-25-flash-image/edit";
    const string END_AURA_SR             = "fal-ai/aura-sr";
    // * 注意:使用 v2.1/master;若你工作區仍是 v2/master,可把下行改為 v2/master
    const string END_KLING_I2V           = "fal-ai/kling-video/v2.1/master/image-to-video";
    const string END_WAN_EFFECTS         = "fal-ai/wan-effects";
    const string END_ANY_LLM             = "fal-ai/any-llm";
    const string END_ELEVENLABS_SFX      = "fal-ai/elevenlabs/sound-effects/v2";
    const string END_CASSETTE_MUSIC      = "cassetteai/music-generator";
    const string END_SONAUTO_MUSIC       = "sonauto/v2/text-to-music";
    const string END_IMAGEN4_FAST        = "fal-ai/imagen4/preview/fast";
    const string END_GEMINI_GEN          = "fal-ai/gemini-25-flash-image";
    const string END_ANY_LLM_VISION      = "fal-ai/any-llm/vision";

    void Awake()
    {
        // 綁定按鈕(Inspector 可只拖想測的)
        SafeBind(btnFastLightningSDXL, () => RunFastLightningSDXL());
        SafeBind(btnTripo3D,            () => RunTripo3D());
        SafeBind(btnFlux,               () => RunFlux());
        SafeBind(btnGeminiEdit,         () => RunGeminiEdit());
        SafeBind(btnAuraSR,             () => RunAuraSR());
        SafeBind(btnKlingImageToVideo,  () => RunKlingImageToVideo());
        SafeBind(btnWanEffects,         () => RunWanEffects());
        SafeBind(btnAnyLLM,             () => RunAnyLLM());
        SafeBind(btnElevenlabsSFX,      () => RunElevenlabsSFX());
        SafeBind(btnCassetteMusic,      () => RunCassetteMusic());
        SafeBind(btnSonautoMusic,       () => RunSonautoMusic());
        SafeBind(btnImagen4Fast,        () => RunImagen4Fast());
        SafeBind(btnGeminiGenerate,     () => RunGeminiGenerate());
        SafeBind(btnAnyLLMVision,       () => RunAnyLLMVision());
    }

    void SafeBind(Button b, Action act)
    {
        if (b != null) b.onClick.AddListener(() => act?.Invoke());
    }

    // ===================== 範例呼叫 =====================

    // 1. Fast Lightning SDXL(Text->Image)
    public void RunFastLightningSDXL()
    {
        var prompt = GetPromptOrDefault("a cinematic portrait of a hero under neon rain, ultra-detailed");
        var body = new Dictionary<string, object> {
            { "prompt", prompt }
        };
        StartCoroutine(CallFal(END_FAST_LIGHTNING_SDXL, body, HandleResultGeneric));
    }

    // 2. Tripo3D: Image->3D
    public void RunTripo3D()
    {
        var img = GetImageOrDefault("https://storage.googleapis.com/falserverless/web-examples/wan-effects/cat.jpg");
        var body = new Dictionary<string, object> {
            { "image_url", img }
        };
        StartCoroutine(CallFal(END_TRIPO_3D, body, HandleResultGeneric));
    }

    // 3. FLUX 系列(示例:flux/dev,Text->Image)
    public void RunFlux()
    {
        var prompt = GetPromptOrDefault("award-winning photo of a rhino in suit at a bar, 85mm bokeh");
        var body = new Dictionary<string, object> {
            { "prompt", prompt }
        };
        StartCoroutine(CallFal(END_FLUX_DEV, body, HandleResultGeneric));
    }

    // 4. Gemini 2.5 Flash Image - 編輯(Image Edit)
    public void RunGeminiEdit()
    {
        var img = GetImageOrDefault("https://storage.googleapis.com/falserverless/example_inputs/nano-banana-edit-input.png");
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("replace background with cyberpunk city lights") },
            { "image_urls", new List<string>{ img } }
        };
        StartCoroutine(CallFal(END_GEMINI_EDIT, body, HandleResultGeneric));
    }

    // 5. Aura-SR(Upscale)
    public void RunAuraSR()
    {
        var img = GetImageOrDefault("https://v3.fal.media/files/rabbit/YYbm6L3DaXYHDL1_A4OaL.jpeg");
        var body = new Dictionary<string, object> {
            { "image_url", img },
            { "upscaling_factor", 4 },
            { "overlapping_tiles", true },
            { "checkpoint", "v2" }
        };
        StartCoroutine(CallFal(END_AURA_SR, body, HandleResultGeneric));
    }

    // 6. Kling - Image to Video
    public void RunKlingImageToVideo()
    {
        var img = GetImageOrDefault("https://storage.googleapis.com/falserverless/kling/kling_input.jpeg");
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("snowflakes fall as a car moves forward along the road") },
            { "image_url", img }
        };
        StartCoroutine(CallFal(END_KLING_I2V, body, HandleResultGeneric));
    }

    // 7. Wan Effects(Image->Video with effects)
    public void RunWanEffects()
    {
        var img = GetImageOrDefault("https://storage.googleapis.com/falserverless/web-examples/wan-effects/cat.jpg");
        var body = new Dictionary<string, object> {
            { "subject", GetPromptOrDefault("a cute kitten wearing sunglasses, hip-hop vibe") },
            { "image_url", img }
        };
        StartCoroutine(CallFal(END_WAN_EFFECTS, body, HandleResultGeneric));
    }

    // 8. Any LLM(文字 LLM)
    public void RunAnyLLM()
    {
        var prompt = GetPromptOrDefault("Give me 3 eerie quest ideas for a rogue-like dungeon crawler.");
        var body = new Dictionary<string, object> {
            // model 可省略使用預設,或自行切換 openai/gpt-4o-mini、anthropic/claude-3.5-sonnet 等
            { "model", "google/gemini-2.0-flash-001" },
            { "prompt", prompt },
            { "temperature", 0.7f },
            { "max_tokens", 300 }
        };
        StartCoroutine(CallFal(END_ANY_LLM, body, HandleResultGeneric));
    }

    // 9. ElevenLabs Sound Effects V2(Text->SFX)
    public void RunElevenlabsSFX()
    {
        var body = new Dictionary<string, object> {
            // 有些端點鍵名是 text,保險起見同時帶上 prompt / text(伺服器會取用需要的)
            { "text", GetPromptOrDefault("wind chimes in a haunted temple, stereo, 6 seconds") },
            { "prompt", GetPromptOrDefault("wind chimes in a haunted temple, stereo, 6 seconds") }
        };
        StartCoroutine(CallFal(END_ELEVENLABS_SFX, body, HandleResultGeneric));
    }

    // 10. CassetteAI Music Generator(Text->Music)
    public void RunCassetteMusic()
    {
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("dark cyberpunk synthwave, driving bassline, 120bpm, 30 seconds") }
        };
        StartCoroutine(CallFal(END_CASSETTE_MUSIC, body, HandleResultGeneric));
    }

    // 11. Sonauto V2 Text-to-Music
    public void RunSonautoMusic()
    {
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("epic orchestral trailer with choirs and taiko drums, 20 seconds") }
            // 也支援 lyrics_prompt / tags 等,需時可自行擴充
        };
        StartCoroutine(CallFal(END_SONAUTO_MUSIC, body, HandleResultGeneric));
    }

    // 12. Imagen 4 Fast(Text->Image)
    public void RunImagen4Fast()
    {
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("retro 1960s kitchen product shot, warm soft sunlight, shallow DOF") }
        };
        StartCoroutine(CallFal(END_IMAGEN4_FAST, body, HandleResultGeneric));
    }

    // 13. Gemini 2.5 Flash Image(Text->Image)
    public void RunGeminiGenerate()
    {
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("an explorer robot walking through neon cherry blossoms at night") },
            { "num_images", 1 },
            { "output_format", "jpeg" }
        };
        StartCoroutine(CallFal(END_GEMINI_GEN, body, HandleResultGeneric));
    }

    // 14. Any LLM Vision(Vision LLM)
    public void RunAnyLLMVision()
    {
        var img = GetImageOrDefault("https://fal.media/files/tiger/4Ew1xYW6oZCs6STQVC7V8_86440216d0fe42e4b826d03a2121468e.jpg");
        var body = new Dictionary<string, object> {
            { "prompt", GetPromptOrDefault("Caption this image for a text-to-image model with as much detail as possible.") },
            { "model", "google/gemini-flash-1.5" },
            { "image_urls", new List<string>{ img } },
            { "temperature", 0.2f },
            { "max_tokens", 200 }
        };
        StartCoroutine(CallFal(END_ANY_LLM_VISION, body, HandleResultGeneric));
    }

    // ===================== Fal 呼叫核心 =====================

    IEnumerator CallFal(string endpoint, Dictionary<string, object> body, Action<string> onSuccess)
    {
        if (string.IsNullOrEmpty(falApiKey))
        {
            SetStatus("❌ 請先設定 falApiKey");
            yield break;
        }

        var url = CombineUrl(falBaseUrl, endpoint);
        string json = JObject.FromObject(body).ToString(Formatting.None);
        byte[] payload = Encoding.UTF8.GetBytes(json);

        using (var req = new UnityWebRequest(url, "POST"))
        {
            req.uploadHandler = new UploadHandlerRaw(payload);
            req.downloadHandler = new DownloadHandlerBuffer();
            req.SetRequestHeader("Content-Type", "application/json");
            req.SetRequestHeader("Accept", "application/json");
            req.SetRequestHeader("Authorization", $"Key {falApiKey}");

            SetStatus($"➡️ POST {endpoint} ...");
            yield return req.SendWebRequest();

#if UNITY_2020_2_OR_NEWER
            if (req.result != UnityWebRequest.Result.Success)
#else
            if (req.isNetworkError || req.isHttpError)
#endif
            {
                SetStatus($"❌ {endpoint} 錯誤:{req.responseCode} {req.error}\n{req.downloadHandler.text}");
                yield break;
            }

            var text = req.downloadHandler.text;
            onSuccess?.Invoke(text);
        }
    }

    void HandleResultGeneric(string json)
    {
        try
        {
            // 使用 Newtonsoft 解析
            var token = JToken.Parse(json);

            // 把可能的檔案 URL 都撈出來(image / video / audio / mesh…)
            var urls = ExtractAllUrls(token).ToList();

            // 顯示第一個可用影像
            var imgUrl = urls.FirstOrDefault(IsImageUrl);
            if (!string.IsNullOrEmpty(imgUrl))
            {
                StartCoroutine(LoadTextureToRawImage(imgUrl));
            }

            // 顯示摘要
            string summary = SummarizeFields(token, urls);
            SetStatus($"✅ 完成\n{summary}");
        }
        catch (Exception e)
        {
            SetStatus("⚠️ 解析回應失敗(JSON):\n" + e);
        }
    }

    // ===================== 工具方法 =====================

    string GetPromptOrDefault(string def) =>
        (promptInput != null && !string.IsNullOrWhiteSpace(promptInput.text)) ? promptInput.text : def;

    string GetImageOrDefault(string def) =>
        (imageUrlInput != null && !string.IsNullOrWhiteSpace(imageUrlInput.text)) ? imageUrlInput.text : def;

    string CombineUrl(string baseUrl, string path)
    {
        if (baseUrl.EndsWith("/")) baseUrl = baseUrl.TrimEnd('/');
        return $"{baseUrl}/{path}";
    }

    IEnumerable<string> ExtractAllUrls(JToken token)
    {
        // 1) 任何名為 "url" 的字串欄位
        foreach (var jt in token.DescendantsAndSelf())
        {
            if (jt.Type == JTokenType.Property)
            {
                var prop = (JProperty)jt;
                if (prop.Name.Equals("url", StringComparison.OrdinalIgnoreCase)
                    && prop.Value.Type == JTokenType.String)
                {
                    var u = prop.Value.ToString();
                    if (IsLikelyFileUrl(u)) yield return u;
                }
            }
            else if (jt.Type == JTokenType.String)
            {
                var s = jt.ToString();
                if (IsLikelyFileUrl(s)) yield return s;
            }
        }
    }

    bool IsLikelyFileUrl(string u)
    {
        if (string.IsNullOrEmpty(u)) return false;
        // 常見回傳副檔名
        string[] exts = { ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp",
                          ".mp4", ".mov", ".webm",
                          ".mp3", ".wav", ".flac", ".m4a",
                          ".obj", ".glb", ".gltf", ".fbx" };
        string lower = u.ToLowerInvariant();
        return lower.StartsWith("http") && exts.Any(e => lower.Contains(e));
    }

    bool IsImageUrl(string u)
    {
        string[] exts = { ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp" };
        string lower = u.ToLowerInvariant();
        return exts.Any(e => lower.Contains(e));
    }

    IEnumerator LoadTextureToRawImage(string url)
    {
        using (var req = UnityWebRequestTexture.GetTexture(url))
        {
            yield return req.SendWebRequest();
#if UNITY_2020_2_OR_NEWER
            if (req.result != UnityWebRequest.Result.Success)
#else
            if (req.isNetworkError || req.isHttpError)
#endif
            {
                SetStatus($"⚠️ 載入圖片失敗:{req.error}\n{url}");
                yield break;
            }

            var tex = DownloadHandlerTexture.GetContent(req);
            if (previewImage != null) previewImage.texture = tex;
            SetStatus($"🖼️ 已載入影像:{tex.width}x{tex.height}");
        }
    }

    string SummarizeFields(JToken token, List<string> urls)
    {
        var sb = new StringBuilder();

        // 嘗試抓常見欄位
        JToken images = token.SelectToken("images");
        JToken video = token.SelectToken("video");
        JToken audio = token.SelectToken("audio");
        JToken mesh  = token.SelectToken("mesh") ?? token.SelectToken("model");

        if (images != null) sb.AppendLine($"images: {images.Count()} 個");
        if (video  != null) sb.AppendLine("video: 有");
        if (audio  != null) sb.AppendLine("audio: 有");
        if (mesh   != null) sb.AppendLine("3D: 有");

        if (token.SelectToken("output") != null)
        {
            string txt = token.SelectToken("output").ToString();
            if (!string.IsNullOrEmpty(txt))
            {
                sb.AppendLine("--- output ---");
                sb.AppendLine(Truncate(txt, 500));
            }
        }

        if (urls != null && urls.Count > 0)
        {
            sb.AppendLine("--- urls ---");
            foreach (var u in urls.Take(6)) sb.AppendLine(u);
            if (urls.Count > 6) sb.AppendLine($"(其餘 {urls.Count - 6} 個省略)");
        }

        // 有些模型會給 description(如 Gemini Edit)
        var desc = token.SelectToken("description")?.ToString();
        if (!string.IsNullOrEmpty(desc))
        {
            sb.AppendLine("--- description ---");
            sb.AppendLine(Truncate(desc, 500));
        }

        return sb.ToString().TrimEnd();
    }

    string Truncate(string s, int max)
    {
        if (string.IsNullOrEmpty(s)) return s;
        return s.Length <= max ? s : s.Substring(0, max) + "...";
    }

    void SetStatus(string msg)
    {
        Debug.Log(msg);
        if (statusText != null) statusText.text = msg;
    }
}

// ========== 小工具:JToken 遞迴 ==========
public static class JTokenExtensions
{
    public static IEnumerable<JToken> DescendantsAndSelf(this JToken node)
    {
        yield return node;
        foreach (var child in node.Children())
        {
            foreach (var desc in child.DescendantsAndSelf())
                yield return desc;
        }
    }
}


上一篇
Day 14 - Open AI API X Unity 應用介紹
下一篇
Day 16 - Groq API 免費開源的文本生成模型
系列文
Vibe Unity - AI時代的遊戲開發工作流30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言